from django.conf import settings
import time, datetime

def formatForConnie(run):
    """
    Returns a string suitable for output into the connie2 process.
    Input argument is expected to be an instance of
    connie2_django.connie2.models.ConnieRun.
    """
    levels = uniqueLevels(run)

    connieInput = [
        formatConnieHeader(run, levels),

        formatConnieParticleList(run),

        '#END_OF_FILE'
        ]
    connieInput = '\n'.join(connieInput)

    numPhases = len(run.larvalPhases.all())
    if numPhases > 1:
        # For more than one phase, return a list of inputs instead of
        # a single string:
        connieInput = [connieInput]
        # remember phases start from 1:
        for phase in range(2, numPhases + 1):
            connieInput.append(formatSecondaryConnieHeader(run, phase, levels, phase == numPhases))
    return connieInput

def formatConnieHeader(run, levels):
    """
    Returns a string in the format of the initial header record
    expected by the conine2 process.  Argument is expected to be an
    instance of connie2_django.connie2.models.ConnieRun
    """
    # constant, for now:
    t_increment = 0.1 if run.isSource else -0.1
    # Note, this must be using version 1.6 of the executable for
    # later; earlier versions were susceptible to rounding errors so
    # required the hack of requesting slightly inside the desired
    # bounds.
    lonmin, latmin, lonmax, latmax = run.boundingBox.extent
    # Just the first phase; any others are handled separately:
    phase = run.larvalPhases.get(phaseNumber=1)
    # Any more phases after the first one?
    finalFile = len(run.larvalPhases.all()) == 1
    # restrict ourselves to a max of 2, arbitrarily, since that's all
    # the interface supports at this point:
    # levels = phase.levels.all()[:2]
    # Either no change (0), or if there are multiple (2, for now)
    # levels we have a regular change (1):
    opt_depth = 0 if len(levels) == 1 else 1
    connieHeader = [
        '#HEADER_START',
        '#COMMENTS',
        'Run id %d' % run.id, # TODO: should url-reverse this when we have that functionality going.

        '#KEEP_RUNNING',
        '0' if finalFile else '1'
        ]

    if not finalFile:
        connieHeader.extend([
                '#NEXT_INPUT_FILE',
                '%s'            # place-holder to format-in the next file name
                ])

    connieHeader.extend([
        '#DIR_VELOCITY_FIELDS',
        settings.CONNIE_DATA_DIR,

        '#VELOCITY_FIELDS',
        run.velocityFields.key,

        '#EXTRA_VELOCITY',
        '%f %f' % (phase.eastwardPropulsion, phase.northwardPropulsion)
        ])

    if phase.windspeedPropulsion:
        connieHeader.extend([
                '#WINDAGE_FRACTION',
                str(phase.windspeedPropulsion / 100) # stored as a percentage, but expected as a fraction
                ])

    # optional random walk stuff:
    if phase.randomPropulsion:
        # see https://wiki.csiro.au/confluence/display/connie/random+walk
        # We assume that the user specifies the stddev of the speed,
        # and we truncate at twice this (2 standard deviations)
        randomStdDev = abs(phase.randomPropulsion)
        connieHeader.extend([
                '#WALK_TYPE,WALK_SEED,WALK_GRANULARITY,WALK_LIMIT',
                '1 100 2 %f' % (2 * phase.randomPropulsion),
                '#WALK_GAUSSIAN_SIGMA_VELOCITY',
                '%f' % randomStdDev
                ])

    # depth options:
    connieHeader.extend([
        '#OPTION_DEPTH',
        str(opt_depth),

        '#NUM_DEPTH_POINTS',
        str(len(levels)),
        '#DEPTH_REFERENCE',
        ' '.join(str(l) for l in levels),
        ])
    if opt_depth:
        # latitude; avg (centre) of bounding box and rounded to a multiple of 5:
        lat = int(latmin + latmax) / (2*5) * 5
        # integer arithmetic to calculate the average month:
        month = (run.spawningFromDate.month + run.spawningToDate.month) / 2
        DAYLIGHT_HOURS = SUNRISE_TIMES[lat][month-1] # 0-based month indexing
        phaseLevels = phase.levels.all()[:2]

        for level in phaseLevels:
            # model choices are day,night = 1,2 hence period-1:
            time = DAYLIGHT_HOURS[level.period - 1]
            hr, m = time/100, time%100
            idx = levels.index(level.verticalLevel.depth) + 1
            connieHeader.extend([
                    '#REGULAR_DEPTH_CHANGE',
                    # 1-based depth index, hour, minute, second:
                    '%d %d %d 0' % (idx, hr, m)
                    ])

    # times, recording, etc:
    connieHeader.extend([
        # time specification:
        '#T_INCREMENT',
        str(t_increment),
        '#NUM_INTEG_STEPS',
        str(abs(int(phase.phaseLongevity / t_increment))),

        '#LON_MIN,LAT_MIN,LON_MAX,LAT_MAX',
        '%f %f %f %f' % (lonmin, latmin, lonmax, latmax),

        # Record both final and cumulative stats, so we can cache and
        # replot quickly, but no tracks which we don't currently plot:
        '#RECORD_TRACKS',
        '0',
        '#RECORD_STATS',
        '1',
        '#RECORD_FINAL_POS',
        '1',

        # ignoring MATLAB_OUTPUT_FILE, and using stdout:
        '#TEXT_OUTPUT_FILE',
        'stdout'
        ])

    return '\n'.join(connieHeader)

def formatSecondaryConnieHeader(run, phaseNum, levels, finalFile):
    """Similar to formatConnieHeader above, but for any additional
    phases.  Just needs to specify the next file if it is to keep
    running, the time (length) of this phase, any extra velocities or
    behaviour, and the depths."""
    t_increment = 0.1 if run.isSource else -0.1
    phase = run.larvalPhases.get(phaseNumber=phaseNum)

    connieHeader = [
        '#HEADER_START',
        '#NUM_INTEG_STEPS',
        str(abs(int(phase.phaseLongevity / t_increment))),
        '#KEEP_RUNNING',
        '0' if finalFile else '1'
        ]
    if not finalFile:
        connieHeader.extend([
                '#NEXT_INPUT_FILE',
                '%s'            # place-holder to format-in the next file name
                ])

    connieHeader.extend([
            '#EXTRA_VELOCITY',
            '%f %f' % (phase.eastwardPropulsion, phase.northwardPropulsion)
            ])

    if phase.windspeedPropulsion:
        connieHeader.extend([
                '#WINDAGE_FRACTION',
                str(phase.windspeedPropulsion / 100) # stored as a percentage, but expected as a fraction
                ])

    # optional random walk stuff:
    if phase.randomPropulsion:
        # see https://wiki.csiro.au/confluence/display/connie/random+walk
        # We assume that the user specifies the stddev of the speed,
        # and we truncate at twice this (2 standard deviations)
        randomStdDev = abs(phase.randomPropulsion)
        connieHeader.extend([
                '#WALK_TYPE,WALK_SEED,WALK_GRANULARITY,WALK_LIMIT',
                '1 100 2 %f' % (2 * phase.randomPropulsion),
                '#WALK_GAUSSIAN_SIGMA_VELOCITY',
                '%f' % randomStdDev
                ])
    else:
        # disable random walk (in case it was enabled previously):
        connieHeader.extend([
                '#WALK_TYPE,WALK_SEED,WALK_GRANULARITY,WALK_LIMIT',
                '0 0 0 0'
                ])

    # Vertical behaviours; restrict ourselves to a max of 2 depths,
    # arbitrarily, since that's all the interface supports at this
    # point:
    phaseLevels = phase.levels.all()[:2]
    # Either no change (0), or if there are multiple (2, for now)
    # levels we have a regular change (1), even if both levels are the
    # same in this phase:
    opt_depth = 0 if len(levels) == 1 else 1
    if opt_depth:
        # if there's only one level depth has been set in the initial
        # input file.  Otherwise, if there's more than one check what
        # we use in this phase: if we have two levels (defensive
        # programming; at time of writing there should always be
        # exactly 2) and they're both different, calculate daylight
        # hours, etc.  Otherwise, we still need to use a
        # regular_depth_change, but just an arbitrary time for a
        # single instance.
        if (len(phaseLevels) and phaseLevels[0].verticalLevel.depth != phaseLevels[1].verticalLevel.depth):
            # latitude; avg (centre) of bounding box and rounded to a multiple of 5:
            lonmin, latmin, lonmax, latmax = run.boundingBox.extent
            lat = int(latmin + latmax) / (2*5) * 5
            # integer arithmetic to calculate the average month:
            month = (run.spawningFromDate.month + run.spawningToDate.month) / 2
            DAYLIGHT_HOURS = SUNRISE_TIMES[lat][month-1] # 0-based month indexing

            for level in phaseLevels:
                # model choices are day,night = 1,2 hence period-1:
                time = DAYLIGHT_HOURS[level.period - 1]
                hr, m = time/100, time%100
                idx = levels.index(level.verticalLevel.depth) + 1
                connieHeader.extend([
                        '#REGULAR_DEPTH_CHANGE',
                        # 1-based depth index, hour, minute, second:
                            '%d %d %d 0' % (idx, hr, m)
                        ])
        else:
            idx = levels.index(phaseLevels[0].verticalLevel.depth) + 1
            # Just a single entry with an arbitrary time:
            connieHeader.extend([
                    '#REGULAR_DEPTH_CHANGE',
                    # 1-based depth index, hour, minute, second:
                        '%d 8 0 0' % idx
                    ])

    connieHeader.append('#END_OF_FILE')
    return '\n'.join(connieHeader)


PARTICLES_PER_CELL = 25         # expect this to change to something
                                # more dynamic and based on
                                # heuristics; also cell size.  Will do
                                # for now though...
def formatConnieParticleList(run):
    """
    Returns a string in the format of the particle specifications
    expected by the connie2 process.  The argument is expected to be
    an instance of connie2_django.connie2.models.ConnieRun.  For now,
    it creates a uniformly-distributed arrangement of particles.
    """
    particleList = []
    oneyear = datetime.timedelta(days=365)
    # We start with the first phase and first level (if more than one):
    phase = run.larvalPhases.get(phaseNumber=1)
    level = phase.levels.all()[0]

    # To create deterministic (reproducable) pseudo-random numbers we
    # use the following strategy: the first instance uses the run id
    # as the seed; subsequent instances use a value of -1 (negative
    # values means to use the present state).  Note that this takes
    # effect only after run 3463; run-files regenerated prior to that
    # should have the random seed replaced with 100000.
    rnd_seed = str(run.id)
    for poly in run.regions:      # regions is a multi-polygon; we can iterate over the polygons contained in it:
        lonmin,latmin,lonmax,latmax = poly.extent
        # a separate particle block for each year:
        for year in run.years.all():
            fromDate = run.spawningFromDate.replace(year=year.year)
            toDate   = run.spawningToDate.replace(year=year.year)
            if toDate < fromDate:
                toDate = toDate + oneyear
            particleList.extend([
                    '#PARTICLE_START',
                    '#DIST_TYPE',
                    '1', # '1' means random distribution over space and time
                    '#NUM_PARTICLES',
                    str(PARTICLES_PER_CELL * ((toDate - fromDate).days + 1)),
                    '#SEED_RANDOM',
                    rnd_seed, # (see comment above about seed strategy)

                    '#LON_MIN_INIT,LAT_MIN_INIT',
                    '%f %f' % (lonmin, latmin),
                    '#YEAR_MIN_INIT,MONTH_MIN_INIT,DAY_MIN_INIT,HOUR_MIN_INIT,MINUTE_MIN_INIT',
                    fromDate.strftime('%Y %m %d 0 0'),
                    '#LON_MAX_INIT,LAT_MAX_INIT',
                    '%f %f' % (lonmax, latmax),
                    '#YEAR_MAX_INIT,MONTH_MAX_INIT,DAY_MAX_INIT,HOUR_MAX_INIT,MINUTE_MAX_INIT',
                    toDate.strftime('%Y %m %d 23 59'),

                    '#DEPTH_INIT',
                    str(level.verticalLevel.depth),
                    ])
            rnd_seed = '-1'     # subsequent instances use a negative value, ie present state

    return '\n'.join(particleList)

def uniqueLevels(run):
    "Returns an ordered list of unique levels"
    levels = []
    for phase in run.larvalPhases.all():
        for level in phase.levels.all()[:2]:
            levels.append(level.verticalLevel.depth)
    uniqueLevels = set(levels)
    return sorted(uniqueLevels)

# Sunrise/sunset times by latitude and month.  Main structure is a
# dict indexed by latitude betwee +/-70, in increments of 5 degrees.
# Each value is a sequence of 12 pairs, one for each month, starting
# from January.  The pair components are integers representing sunrise
# and sunset times in 24hr time.  Hour/minute components can be
# accessed with integer division, ie hour = t/100, minutes = t%100.
SUNRISE_TIMES = {
      70: ((1, 2359), (839, 1551), (623, 1757), (356, 2007), (41, 2329), (1159, 1201), (1159, 1201), (256, 2109), (512, 1836), (712, 1617), (949, 1339), (1, 2359)), \
      65: ((941, 1438), (804, 1626), (619, 1800), (425, 1937), (236, 2120), (105, 2257), (158, 2211), (345, 2022), (521, 1827), (653, 1638), (836, 1452), (1004, 1346)), \
      60: ((849, 1530), (741, 1648), (617, 1802), (444, 1918), (323, 2032), (236, 2125), (303, 2107), (414, 1953), (528, 1821), (639, 1651), (758, 1531), (857, 1453)), \
      55: ((817, 1602), (725, 1704), (615, 1804), (457, 1904), (353, 2001), (320, 2041), (341, 2030), (434, 1933), (532, 1817), (629, 1701), (732, 1557), (819, 1531)), \
      50: ((753, 1625), (713, 1717), (614, 1805), (507, 1854), (414, 1939), (350, 2011), (407, 2004), (449, 1918), (536, 1814), (622, 1709), (713, 1616), (752, 1558)), \
      45: ((735, 1643), (703, 1726), (613, 1806), (516, 1845), (431, 1922), (413, 1948), (427, 1944), (501, 1907), (538, 1811), (615, 1715), (657, 1631), (731, 1619)), \
      40: ((720, 1658), (654, 1735), (612, 1807), (522, 1838), (445, 1908), (431, 1930), (444, 1928), (511, 1857), (541, 1809), (610, 1721), (645, 1644), (715, 1636)), \
      35: ((708, 1711), (647, 1742), (611, 1808), (528, 1832), (457, 1857), (446, 1915), (457, 1914), (520, 1849), (543, 1807), (606, 1725), (634, 1655), (700, 1650)), \
      30: ((657, 1722), (641, 1748), (610, 1808), (533, 1827), (507, 1846), (458, 1902), (509, 1903), (527, 1841), (544, 1805), (602, 1729), (625, 1704), (648, 1702)), \
      25: ((647, 1731), (635, 1754), (609, 1809), (538, 1822), (516, 1837), (510, 1851), (520, 1852), (534, 1835), (546, 1804), (558, 1733), (617, 1712), (637, 1713)), \
      20: ((638, 1741), (630, 1759), (609, 1810), (542, 1818), (524, 1829), (520, 1841), (529, 1843), (540, 1829), (547, 1803), (555, 1737), (609, 1720), (627, 1723)), \
      15: ((629, 1749), (625, 1804), (608, 1810), (546, 1814), (532, 1821), (530, 1831), (538, 1834), (546, 1823), (549, 1802), (552, 1740), (601, 1728), (618, 1733)), \
      10: ((621, 1757), (620, 1808), (607, 1811), (550, 1810), (539, 1814), (539, 1822), (546, 1825), (551, 1818), (550, 1800), (548, 1743), (554, 1735), (609, 1742)), \
       5: ((613, 1805), (615, 1813), (606, 1812), (553, 1807), (546, 1807), (548, 1813), (554, 1817), (556, 1813), (551, 1759), (545, 1746), (548, 1741), (600, 1750)), \
       0: ((605, 1813), (611, 1818), (606, 1812), (557, 1803), (553, 1800), (557, 1804), (602, 1810), (601, 1808), (552, 1758), (542, 1749), (541, 1748), (551, 1759)), \
      -5: ((558, 1821), (606, 1822), (605, 1813), (600, 1800), (600, 1753), (605, 1755), (610, 1802), (606, 1803), (553, 1757), (539, 1752), (534, 1755), (543, 1808)), \
     -10: ((550, 1829), (601, 1827), (604, 1814), (604, 1756), (607, 1746), (614, 1747), (618, 1754), (611, 1758), (554, 1756), (536, 1755), (527, 1802), (534, 1816)), \
     -15: ((541, 1837), (556, 1832), (603, 1814), (607, 1753), (614, 1739), (623, 1738), (626, 1746), (616, 1753), (555, 1755), (533, 1759), (520, 1809), (525, 1826)), \
     -20: ((536, 1845), (554, 1834), (604, 1811), (612, 1745), (623, 1730), (634, 1729), (634, 1739), (618, 1749), (552, 1756), (526, 1804), (512, 1820), (517, 1838)), \
     -25: ((523, 1855), (546, 1842), (601, 1816), (615, 1745), (629, 1723), (643, 1718), (644, 1728), (628, 1742), (557, 1753), (526, 1806), (505, 1825), (505, 1846)), \
     -30: ((513, 1905), (540, 1848), (600, 1817), (619, 1741), (638, 1715), (654, 1707), (654, 1718), (634, 1736), (558, 1752), (522, 1810), (456, 1834), (453, 1857)), \
     -35: ((501, 1917), (533, 1855), (559, 1818), (624, 1736), (647, 1705), (706, 1655), (706, 1707), (641, 1729), (600, 1751), (518, 1814), (446, 1844), (440, 1910)), \
     -40: ((448, 1930), (525, 1903), (557, 1820), (629, 1731), (658, 1654), (720, 1641), (718, 1654), (648, 1721), (601, 1750), (513, 1820), (435, 1855), (425, 1925)), \
     -45: ((432, 1946), (516, 1912), (556, 1821), (635, 1724), (711, 1641), (737, 1624), (733, 1639), (657, 1712), (603, 1748), (507, 1826), (421, 1909), (407, 1943)), \
     -50: ((412, 2006), (505, 1923), (554, 1823), (642, 1717), (727, 1626), (758, 1603), (752, 1620), (708, 1701), (605, 1747), (459, 1833), (405, 1925), (345, 2005)), \
     -55: ((346, 2031), (451, 1936), (551, 1826), (651, 1708), (746, 1606), (824, 1536), (815, 1557), (722, 1648), (607, 1745), (451, 1842), (344, 1947), (315, 2035)), \
     -60: ((309, 2108), (432, 1955), (548, 1829), (703, 1656), (813, 1539), (903, 1458), (849, 1524), (740, 1630), (610, 1742), (439, 1854), (315, 2016), (231, 2119)), \
     -65: ((205, 2210), (406, 2020), (543, 1833), (718, 1640), (853, 1459), (1010, 1350), (942, 1431), (805, 1605), (613, 1739), (423, 1911), (230, 2102), (101, 2251)), \
     -70: ((1, 2359), (323, 2102), (536, 1840), (742, 1616), (1011, 1341), (1159, 1201), (1159, 1201), (844, 1526), (619, 1734), (358, 1936), (49, 2251), (1, 2359))
    }
